System Design_大型网站伸缩性架构

网站架构的伸缩性设计

网站的伸缩性是指不需要改变网站的软硬件设计,仅仅通过增减部署的服务器数量就可以扩大或缩小网站的服务处理能力。这样就可以做到通过不断简单地向集群中添加服务器来增强整个集群的能力。

一般说来,网站的伸缩性设计可分为两类,一类是根据功能进行物理分离是实现伸缩,一类是单一功能通过集群实现伸缩。前者是不同的服务器部署不同的服务,提供不同的功能;后者是集群内的多台服务器部署相同的服务,提供相同的功能。

不同功能进行物理分离实现伸缩

使用新增的服务器处理某种特定服务,通过物理上分离不同的功能,实现网站伸缩。
System_design_c6_1

具体又可分为如下两种情况:

纵向分离(垂直分割):将业务处理流程上的不同部分分离部署,实现系统伸缩性。
System_design_c6_2

横向分离(功能分割):将不同的业务模块分离部署,实现系统伸缩性。
System_design_c6_3

单一功能通过集群实现伸缩

将不同功能分离部署可以实现一定程度的伸缩性,但是随着网站并发访问逐步增加,单一的功能独立部署在独立的服务器上也不能满足业务规模的要求。因此必须使用服务器集群,即将相同服务部署在多台服务器上构成一个集群整体对外提供服务。

集群伸缩又具体可分为应用服务器集群伸缩性和数据服务器伸缩性,技术实现差别很大。而数据服务器集群也可分为缓存数据服务器集群和存储数据服务器集群,其伸缩性设计也很不一样。

应用服务器集群的伸缩性设计

在构建高可用的架构时曾提到,应用服务器应该设计成无状态的,即应用服务器不存储请求上下文信息,这样每次用户请求可以由任意一台服务器处理,而不会影响结果。

如果HTTP请求分发装置可以感知或配置集群的服务器数量,及时发现集群中新上线或下线的服务器,并分发请求,就实现了应用服务器集群的伸缩性。

这里,这个HTTP请求分发装置被称为负载均衡服务器。

负载均衡是网站必不可少的基础技术手段,不但可以实现网站的伸缩性,同时还可改善网站的可用性,

System_design_c6_4

HTTP重定向负载均衡

使用HTTP重定向协议实现负载均衡。

System_design_c6_5

HTTP重定向服务器是一台普通的应用服务器,功能是更具用户的HTTP请求计算一台真实的Web服务器地址,并使浏览器重定向到该服务器。

优点是简单,缺点是浏览器需要两次请求服务器才能完成一次访问。另外重定向服务器本身可能称为瓶颈。

DNS域名解析负载均衡

利用DNS处理域名解析请求的同时进行负载均衡处理的一种方案。

System_design_c6_6

在DNS服务器中配置多个A记录,每次域名解析请求都会根据负载均衡算法计算一个不同的IP地址返回,这样A记录配置的多个服务器就构成一个集群,并实现负载均衡。

优点是将工作转交给了DNS,同时许多DNS还支持基于地里位置的域名解析,可以实现选择最近的一个服务器地址,加快访问速度。缺点是目前的DNS是多级解析,每一级DNS都可能缓存A记录,这样下线了某台服务器后,即使修改了DNS的A记录,使其生效也需要较长一段时间,这段时间访问该服务器的请求都会失败。

大型网站总是部分使用DNS域名解析,利用域名解析作为第一级负载均衡手段,即域名解析得到的服务器地址并不是实际提供服务的应用服务器,而是同样提供负载均衡服务的内部服务器,这组内部负载均衡服务器再进行负载均衡,将请求分发到真正提供服务的Web服务器上。

反向代理负载均衡

System_design_c6_7

可以使用反向代理实现缓存资源,过滤恶意访问。同时还可以提供负载均衡的功能,管理一组Web服务器,将请求根据负载均衡转发到不同的Web服务器上。Web服务器处理完成的响应也需要通过反向代理服务器返回给用户。由于Web服务器不直接对外提供访问,因此不需要使用外部IP地址,而反向代理服务器需要配置双网卡和内部外部两套IP地址。

由于反向代理服务器转发请求在HTTP协议层面,因此也称为应用层负载均衡。优点是和反向代理服务器功能集成在一起,部署简单。缺点是方向代理服务器是所有请求和响应的中转站,其性能可能会成为瓶颈。

IP负载均衡

在网络层通过修改请求目标地址进行负载均衡。

System_design_c6_8

负载均衡服务器在操作系统内核进程获取网络数据包,根据负载均衡算法得到一个真实Web服务器IP地址,然后将数据目的地IP修改为该地址,不需要用户进程处理。Web服务器处理完后响应数据包回到负载均衡服务器,负载均衡服务器再将数据报源地址改为自己的地址。

关键是Web服务器响应数据包如何返回给负载均衡服务器,一个方案是负载均衡服务器在修改目的IP地址时同时修改源地址,需要记录真正的源地址。另一种是将负载均衡服务器同时作为真实物理服务器集群的网关服务器,这样所有响应数据都会到达负载均衡服务器。

IP负载均衡在内核进程完成数据分发,较反向代理负载均衡有更好的处理性能。但是由于所有请求响应都需要经过负载均衡服务器,集群的最大响应数据量受限于负载均衡服务器的网卡带宽,在很多情况下可能难以满足需求。

数据链路层负载均衡

数据链路层负载均衡是指在通信协议的数据链路层修改mac地址进行负载均衡。

System_design_c6_9

这种数据传输方式又称为三角传输模式,负载均衡数据分发过程中不修改IP地址,只修改目的mac地址,通过配置真实物理服务器集群所有机器虚拟IP和负载均衡服务器IP地址一致,从而达到不修改数据包的源地址和目的地址就可以进行数据分发的目的,由于实际处理请求的真实物理服务器IP和数据请求目的IP一致,不需要通过负载均衡服务器进行地址转换,可将响应数据包返回给用户浏览器(应用服务器通常是设置为只允许发送数据包出内网,而不能直接接收外网的请求,也就是外网唯一内访问到的仍只有负载均衡服务器),避免负载均衡服务器网卡带宽成为瓶颈。这种负载均衡方式也称为直接路由方式。

链路层负载均衡是目前大型网站使用最广的一种负载均衡手段,在Linux平台上最好的链路层负载均衡开源产品是LVS(Linux Virtual Server)。

负载均衡算法

负载均衡服务器实现可以分为两个部分:

  1. 根据负载均衡算法和Web服务器列表计算得到一台服务器地址。
  2. 将请求数据发送到该地址对应的Web服务器上。

前面介绍了如何将请求数据发送到web服务器上,下面是几种负载均衡算法:

  • 轮询(Round Robin)
    所有请求依次被分发到每台应用服务器上,即每台服务器需要处理的请求数目都相同,适用于所有服务器情况都相同的场景。

  • 加权轮询(Weighted Round Robin)
    根据服务器性能,在轮询基础上,按照配置的权重分发请求。

  • 随机(Random)
    另外也有加权随机算法

  • 最少连接
    记录每个服务器正在处理的请求数,发送到最少请求的服务器上。也可以进行加权。

  • 源地址散列
    根据请求来源的IP地址进行Hash计算,得到应用服务器,这样来自同一个IP地址的请求总是在同一个服务器上处理,该请求的上下文信息可以存储在这台服务器上,在一个会话周期内重复使用,实现会话粘滞。

分布式缓存集群的伸缩性设计

和所有服务器都部署相同应用的应用服务器集群不同,分布式缓存服务器集群中不同服务器中缓存的数据各不相同,缓存访问请求不能任意发送到任意一台缓存服务器,必须先找到缓存数据所在的服务器,然后才能访问。这个特点会严重制约分布式缓存集群的伸缩性设计,因为新上线的缓存服务器没有任何数据,而下线的缓存服务器还缓存着许多热点数据。

必须让新上线的缓存服务器对整个分布式缓存集群影响最小,也就是是说新加入缓存服务器后必须使整个缓存服务器集群已经缓存的数据尽可能还能被访问到,也是分布式缓存集群伸缩性设计的主要目标。

Memcached分布式缓存集群的访问模型

System_design_c6_10

由路由算法根据应用程序输入的缓存数据KEY计算得到应该将数据写入哪台服务器或者应该从哪台服务器读取数据。

Memcached分布式缓存集群的伸缩性挑战

对于缓存服务器集群的管理,路由算法至关重要,相当与前面的负载均衡算法一样,决定这该访问集群中的哪台服务器。

简单的路由算法可以使用余数Hash,或者加权的余数Hash路由。如果不考虑缓存服务器集群伸缩性,余数Hash基本可以满足需求

问题是,当分布式缓存集群需要扩容时,之前已缓存的数据的重新计算的Hash由于服务器数量的增加都发生改变,也就是之前已缓存在其他服务器上的数据也无法被访问到了。

一种解决方法是在网站访问量最少是扩容集群,负载冲击最小。然后通过模拟请求的方法逐渐预热缓存,使缓存服务器中的数据重新分布。

更好的解决方案是一致性Hash算法,可以使得新加入的服务器不影响大部分已缓存数据的正确命中。

分布式缓存的一致性Hash算法

一致性Hash算法通过一个叫作一致性Hash环的数据结构实现KEY到缓存服务器的Hash映射。

System_design_c6_11

具体算法是:先构造一个长度为$0-2^{32}$的整数环(一致性Hash环),根据节点名称的Hash值,将缓存服务器节点放置在这个Hash环上。然后根据需要缓存的数据的KEY值计算得到其Hash值,然后在Hash环上顺指针查找距离这个KEY的Hash值最近的缓存服务器节点,完成KEY到服务器的Hash映射查找。

当缓存服务器集群需要扩容时,只需要将新加入的节点名称的Hash放入一致性Hash环中,由于KEY是顺时针查找距离其最近的节点,因此新加入的节点只影响整个环中的一小段。并且随着集群规模越大继续命中原有缓存的概率也也逐渐增大。

System_design_c6_12

具体应用中,这个一致性Hash环通常使用BST实现,Hash查找过程实际上是在BST查找不小于查找数的最小数值。当然这个二叉树的最右边叶子结点和最左边叶子节点相连接,构成环。

目前解决方案存在的问题是,可能会造成负载不均衡。如上面新加入的NODE3只影响了原来的节点NODE1,但是原来的节点NODE0和NODE2不受影响,意味着NODE0和NODE2缓存压力是NODE1与NODE3的两倍。

计算机的任何问题都可以通过增加一个虚拟层来解决。

上面一致性Hash算法带来的负载不均衡问题也可以通过使用虚拟层的手段,将每台物理缓存服务器虚拟为一组虚拟缓存服务器,将虚拟服务器的Hash值放置在Hash环上,KEY在环上先找到虚拟服务器节点,再得到服务器的信息。

这样新加入物理服务器节点时,是将一组虚拟节点加入环中,如果虚拟节点的数目足够多,这组虚拟节点将会影响同样多数目的已经存在的虚拟节点,这些已经存在的虚拟节点又对应不同的物理节点。最终的结果是:新加入一台缓存服务器,将会较为均匀的影响原来集群中已经存在的所有服务器。其导致的命中失败影响跟不使用虚拟节点时相同。

System_design_c6_13

每个物理节点对应的虚拟节点越多,各个物理节点之间的负载越均衡,新加入物理服务器对原有的物理服务器影响越保持一致。过多也会影响性能,一般设置为150个。

数据存储服务器集群的伸缩性设计

数据存储服务器必须保证数据的可靠存储,任何情况下都必须保证数据的可用性和正确性。因此缓存服务集群的伸缩性架构不能直接适用与数据库等存储服务器。

具体的,数据存储服务器集群的伸缩性设计又可分为关系数据库集群的伸缩性设计和NoSQL数据库的伸缩性设计。

关系数据库集群的伸缩性设计

最简单的是使用数据复制功能对数据库进行伸缩:

System_design_c6_14

也就是数据库主从读写分离,另外前面提到的业务分割模式也可以用在数据库,不同业务的数据表部署在不同数据库上,即数据分库,这种方式的制约条件是跨库的表不能进行join操作。

在大型网站的实际应用中,即使进行了分库和主从复制,对一些单表数据仍然很大的表,比如Facebook的用户数据库等,还需要进行分片,也就是将一张表拆开分别存储在多个数据库中。

目前支持数据分片的分布式数据库产品主要有Amoeba和Cobar。

System_design_c6_15

Cobar是一个分布式关系数据库访问代理,介于应用服务器和数据库服务器之间(Cobar也支持非独立部署,以lib的方式和应用程序部署在一起)。应用程序通过JDBC驱动访问Cobar集群,Cobar服务器根据SQL和分库规则分解SQL,分发到MySQL集群的不同数据库实例上执行(每个MySQL实例部署为主/从结构,保证数据高可用)。

Cobar系统组件模型如下:

System_design_c6_16

前端通信模块负责和应用程序通信,收到SQL请求select * from users where userid in(12,22,23)后转交给SQL解析模块,SQL解析模块解析获得SQL中路由规则查询条件userid in(12,22,23)转交给SQL路由模块,SQL路由模块根据路由配置规则(userid为偶数路由至数据库A,为奇数路由至数据库B)将应用程序提交的SQL分解成两条SQL(select * from user where userid in(12,22);select * from user where userid in(23))然后转交给SQL执行代理模块,发送到数据库A和数据库B分别执行。

数据库A和数据库B的执行结果返回至SQL执行模块,通过结果合并模块将两个返回结果集合并成一个结果集,最终返回给应用程序。

Cobar如何进行集群的伸缩

Cobar的伸缩有两种:Cobar服务器集群的伸缩和MySQL服务器集群的伸缩。

Cobar服务器可以看做是无状态的应用服务器,上面的负载均衡手段可以直接应用实现集群伸缩。而MySQL中存储这数据,要想保证集群扩容后数据一致负载均衡,必须要做数据迁移,将集群中原来机器中的数据迁移到新添加的机器中:

System_design_c6_17

具体迁移哪些数据可以利用一致性Hash算法,尽量使需要迁移的数据最少。实践中,Cobar利用的MySQL的数据同步功能进行数据迁移,数据迁移不是以数据为单位,而是以Schema为单位。集群扩容时,从每个服务器中迁移部分Schema到新机器中,使用MySQL的同步机制。

System_design_c6_18

同步完成时,及新机器中Schema数据和原机器中Schema数据一致时,修改Cobar服务器的路由配置,将这些Schema的IP修改为新机器的IP,然后删除原机器中的相关Schema,完成MySQL集群扩容。

但是同时这也限制了关系数据库某些功能的使用,例如无法执行跨库Join操作等,更不能执行跨库的事务处理。

NoSQL数据库的伸缩性设计

NoSQL主要指非关系的、分布式的数据库设计模式,是关系数据库的补充,而不是替代方案。NoSQL数据库产品放弃了关系数据库的两大重要基础:SQL和事物一致性保证(ACID),以强化其他一些大型网站更关注的特性:高可用性和伸缩性。

目前应用最广泛的是Apache HBase,HBase为可伸缩海量数据存储而设计,实现面向在线业务的实时数据访问延迟。HBase的伸缩性主要依赖其可分裂HRegion及可伸缩的分布式文件系统HDFS实现。

System_design_c6_19

HBase中,数据以HRegion为单位进行管理,也就是应用程序如果要访问一个数据,必须先找到HRegion,然后将数据读写操作提交给HRegion,由HRegion完成存储层面的数据操作。每个HRegion中存储一段Key值区间[key1, key2)的数据,HRegionServer是物理服务器,每个HRegionSever上可以启动多个HRegion实例。当一个HRegion中的写入数据太多,达到配置的阀值时,HRegion会分裂成两个HRegion,并将HRegion在整个集群中进行迁移,以实现HRegionSever的负载均衡。

所有HRegion的信息(存储的Key区间,所在HRegionSever地址,访问端口号等)都记录在HMaster服务上,为了保证高可用,HBase启动多个HMaster,并通过Zookeeper(一个支持分布式一致性的数据管理服务)选举出一个主服务器,应用程序通过Zookeeper获得主HMaster的地址,输入Key值获得这个Key所在的HRegioinServer地址,然后请求HRegionServer上的HRegion,获得需要的数据。调用时序如下:

System_design_c6_20

写入过程也是一样,需要先得到HRegion才能继续操作,HRegion会把数据存储在若干个叫HFile格式的文件中,这些文件使用HDFS分布式文件系统存储,在整个集群内分布并高可用。当一个HRegino中数据太多时,HRegion(连同HFile)会分裂成两个HRegion,并根据集群中服务器负载进行迁移,如果集群中有新加入的服务器,也就是有新的HRegionServer,由于其负载较低,也会把HRegion迁移过去并记录到HMaster,从而实现HBase的线程伸缩。